package jxtend.eclipse.jdt.hook;

import static java.lang.Character.isDigit;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.TypeElement;

import jxtend.processor.eclipse.JxtendProcessor;
import jxtend.processor.eclipse.utils.EclipseProcessorUtils;
import jxtend.processor.reflect.ReflectField;
import jxtend.processor.utils.ReflectUtils;

import org.eclipse.core.internal.preferences.EclipsePreferences;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.INodeChangeListener;
import org.eclipse.jdt.apt.core.internal.AnnotationProcessorFactoryLoader;
import org.eclipse.jdt.apt.core.internal.IServiceFactory;
import org.eclipse.jdt.apt.core.internal.util.FactoryPath.Attributes;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.WorkingCopyOwner;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.Compiler;
import org.eclipse.jdt.internal.compiler.ICompilerRequestor;
import org.eclipse.jdt.internal.compiler.IErrorHandlingPolicy;
import org.eclipse.jdt.internal.compiler.IProblemFactory;
import org.eclipse.jdt.internal.compiler.apt.dispatch.BaseProcessingEnvImpl;
import org.eclipse.jdt.internal.compiler.apt.dispatch.RoundEnvImpl;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.env.ICompilationUnit;
import org.eclipse.jdt.internal.compiler.env.INameEnvironment;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.impl.ITypeRequestor;
import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.parser.Parser;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
import org.eclipse.jdt.internal.core.CompilationUnit;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.JavaProject;
import org.eclipse.jdt.internal.core.SearchableEnvironment;
import org.eclipse.jdt.ui.PreferenceConstants;

@SuppressWarnings("restriction")
public class AnnotationProcessorInvoker {

	private static final ReflectField<RoundEnvImpl, CompilationUnitDeclaration[]> unitsOfRoundEnvImpl =
			ReflectField.getField(RoundEnvImpl.class, CompilationUnitDeclaration[].class, "_units");

	private static final Class<IEclipsePreferences.INodeChangeListener> anonymousPreferencesNodeListener; static {
		try {
			anonymousPreferencesNodeListener =
					(Class<INodeChangeListener>) JavaProject.class.forName(JavaProject.class.getName() + "$1");
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
	}

	private static final ReflectField<JavaProject, IEclipsePreferences.INodeChangeListener>
		preferencesNodeListenerOfJavaProject = ReflectField.getField(JavaProject.class,
				IEclipsePreferences.INodeChangeListener.class, "preferencesNodeListener");
	private static final ReflectField<IEclipsePreferences.INodeChangeListener, IEclipsePreferences>
		eclipsePreferencesOfAnonymousPreferencesNodeListener = ReflectField.getField(anonymousPreferencesNodeListener,
				IEclipsePreferences.class, "val$eclipsePreferences");
	private static final ReflectField<EclipsePreferences, org.eclipse.core.internal.preferences.ImmutableMap>
		propertiesOfEclipsePreferences = ReflectField.getField(EclipsePreferences.class,
				org.eclipse.core.internal.preferences.ImmutableMap.class, "properties");
	
    private static ThreadLocal<Integer> nestedCount = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() { return 0; }
    };

    private static ThreadLocal<Set<CompilationUnitDeclaration>> delayedCompilationUnitDeregistrations =
    		new ThreadLocal<Set<CompilationUnitDeclaration>>();

    private static ThreadLocal<ExistingLookupEnvironmentInfo> existingLookupEnvironmentInfo =
    		new ThreadLocal<ExistingLookupEnvironmentInfo>();

    public static boolean isProcessingInCurrentThread() {
    	return (nestedCount.get() > 0);
    }

	public static void invoke(Parser parser, ICompilationUnit sourceUnit, CompilationUnitDeclaration parsedUnit,
			LookupEnvironment providedLookupEnvironment) {
		invoke(parser, sourceUnit, parsedUnit, providedLookupEnvironment, true, true);
	}
    
	public static void invoke(Parser parser, ICompilationUnit sourceUnit, CompilationUnitDeclaration parsedUnit,
			LookupEnvironment providedLookupEnvironment, boolean completeTypeBindings, boolean deregisterAtEnd) {
	
		if ((parsedUnit.types == null) || !(sourceUnit instanceof CompilationUnit)) {
			return;
		}

		JavaProject javaProject = EclipseHookUtils.extractJavaProject(sourceUnit);

		invoke(parser, javaProject, ((CompilationUnit) sourceUnit).owner, parsedUnit, providedLookupEnvironment,
				completeTypeBindings, deregisterAtEnd);
	}

	private static EclipsePreferences getEclipsePreferences(JavaProject javaProject) {
		IEclipsePreferences.INodeChangeListener preferencesNodeListener =
				preferencesNodeListenerOfJavaProject.get(javaProject);
		if (preferencesNodeListener != null) {
			return (EclipsePreferences) eclipsePreferencesOfAnonymousPreferencesNodeListener.get(
					preferencesNodeListener);
		}

		return (EclipsePreferences) javaProject.getEclipsePreferences();
	}

	private static String getString(EclipsePreferences eclipsePreferences, String key) {
		String value = getOptionalString(eclipsePreferences, key);
		if (value == null) {
			throw new IllegalArgumentException("Couln't find a value for preference key: " + key);
		}
		return value;
	}

	private static String getOptionalString(EclipsePreferences eclipsePreferences, String key) {
		String value = eclipsePreferences.get(key, null);
		if (value == null) {
			value = JavaModelManager.getJavaModelManager().getInstancePreferences().get(key, null);
		}

		return value;
	}
	
	private static IServiceFactory getAnnotationProcessorServiceFactory(
			JavaProject javaProject, String jsr269AnnotationProcessorClassName) {
		Map<IServiceFactory, Attributes> java6FactoriesAndAttributesForProject =
				AnnotationProcessorFactoryLoader.getLoader().getJava6FactoriesAndAttributesForProject(javaProject);
		for (Map.Entry<IServiceFactory, Attributes> entry : java6FactoriesAndAttributesForProject.entrySet()) {
			IServiceFactory serviceFactory = entry.getKey();
			Attributes attributes = entry.getValue();

			if (serviceFactory.toString().equals(jsr269AnnotationProcessorClassName) && attributes.isEnabled()) {
				return serviceFactory;
			}
		}

		return null;
	}

	public static void addDelayedCompilationUnitDeregistration(CompilationUnitDeclaration compilationUnitDeclaration) {
		Set<CompilationUnitDeclaration> delayedCompilationUnitDeregistrationList =
				delayedCompilationUnitDeregistrations.get();
		if (delayedCompilationUnitDeregistrationList == null) {
			delayedCompilationUnitDeregistrationList = new HashSet<CompilationUnitDeclaration>();
			delayedCompilationUnitDeregistrations.set(delayedCompilationUnitDeregistrationList);
		}

		delayedCompilationUnitDeregistrationList.add(compilationUnitDeclaration);
	}

	public static void invoke(Parser parser, JavaProject javaProject, WorkingCopyOwner workingCopyOwner,
			CompilationUnitDeclaration parsedUnit, LookupEnvironment providedLookupEnvironment,
			boolean completeTypeBindings, boolean deregisterAtEnd) {
		EclipsePreferences eclipsePreferences = getEclipsePreferences(javaProject);
		String sourceLevel = getString(eclipsePreferences, "org.eclipse.jdt.core.compiler.source");
		assert sourceLevel.length() == 3
				&& sourceLevel.charAt(0) == '1' && sourceLevel.charAt(1) == '.' && isDigit(sourceLevel.charAt(2));
		if (sourceLevel.charAt(2) <= '5') {
			return;
		}

		assert sourceLevel.charAt(2) >= 6;

		String processAnnotationsPreference =
				getOptionalString(eclipsePreferences, "org.eclipse.jdt.core.compiler.processAnnotations");

		boolean processAnnotations =
				(processAnnotationsPreference != null) ? processAnnotationsPreference.equals("enabled") : false;
		if (!processAnnotations) {
			return;
		}

		IServiceFactory jxtendProcessorServiceFactory =
				getAnnotationProcessorServiceFactory(javaProject, JxtendProcessor.class.getName());
		if (jxtendProcessorServiceFactory == null) {
			return;
		}

		if ((parsedUnit.bits & ASTNode.Bit21) != 0) {
			return;
		}
		
		PreferenceConstants.getPreferenceStore().setValue(PreferenceConstants.CODEASSIST_FILL_ARGUMENT_NAMES, true);

		nestedCount.set(nestedCount.get() + 1);

//		ExistingLookupEnvironmentInfo foundExistingLookupEnvironmentInfo = existingLookupEnvironmentInfo.get();

		try {
			LookupEnvironment lookupEnvironment = null;
			if (providedLookupEnvironment != null) {
//					&& (foundExistingLookupEnvironmentInfo == null
//							|| foundExistingLookupEnvironmentInfo.environment != providedLookupEnvironment)) {
//				System.out.println("Reusing existing lookup environment");
//				if (Thread.currentThread().getName().equals("org.eclipse.jdt.internal.ui.text.JavaReconciler")) {
//					existingLookupEnvironmentInfo.set(new ExistingLookupEnvironmentInfo(providedLookupEnvironment));
//				}

//				foundExistingLookupEnvironmentInfo = existingLookupEnvironmentInfo.get();
//
//				if (foundExistingLookupEnvironmentInfo != null) {
//					lookupEnvironment = foundExistingLookupEnvironmentInfo.environment;
//				} else {
					lookupEnvironment = providedLookupEnvironment;
//				}

				if (completeTypeBindings) {
					lookupEnvironment.buildTypeBindings(parsedUnit, null);
					lookupEnvironment.completeTypeBindings(parsedUnit, true);
				}
			} else {
//				foundExistingLookupEnvironmentInfo = existingLookupEnvironmentInfo.get();
//						(Thread.currentThread().getName().equals("org.eclipse.jdt.internal.ui.text.JavaReconciler")
//								|| Thread.currentThread().getName().equals("main"))
//							? existingLookupEnvironmentInfo.get() : null;
//				if (providedLookupEnvironment == null &&
//				if (foundExistingLookupEnvironmentInfo != null) {
//					foundExistingLookupEnvironmentInfo.nestingCount++;
//
////					System.out.println("Reusing existing lookup environment (from thread local; nested count = "
////							+ foundExistingLookupEnvironmentInfo.nestingCount + ")");
////
////					lookupEnvironment = foundExistingLookupEnvironmentInfo.environment;
//				}
//				else {
//					System.out.println("Creating a lookup environment");
//					if (Thread.currentThread().getName().equals("org.eclipse.jdt.internal.ui.text.JavaReconciler")) {
//							|| Thread.currentThread().getName().equals("main")) {
//						existingLookupEnvironmentInfo.set(null);
//					}

					Map<String, Object> compilerSettings = new HashMap<String, Object>();
					compilerSettings.put(CompilerOptions.OPTION_Source, sourceLevel);

					lookupEnvironment = createLookupEnvironment(
							javaProject, workingCopyOwner, new CompilerOptions(compilerSettings));

//					if (foundExistingLookupEnvironmentInfo == null && Thread.currentThread().getName().equals("main")) {
//						existingLookupEnvironmentInfo.set(new ExistingLookupEnvironmentInfo(lookupEnvironment));
//					}
//				}

				lookupEnvironment.buildTypeBindings(parsedUnit, null);
				lookupEnvironment.completeTypeBindings(parsedUnit, true);
			}

			BaseProcessingEnvImpl processingEnvironment = new HookProcessingEnvironment(lookupEnvironment);
	
			Processor annotationProcessor = createAnnotationProcessor(jxtendProcessorServiceFactory);
			if (annotationProcessor instanceof jxtend.processor.eclipse.JxtendProcessor) {
				annotationProcessor.init(processingEnvironment);

				RoundEnvironment roundEnvironment = new HookRoundEnvironment(
						new CompilationUnitDeclaration[0], new ReferenceBinding[0], false, processingEnvironment);

				unitsOfRoundEnvImpl.set(
						(RoundEnvImpl) roundEnvironment, new CompilationUnitDeclaration[] { parsedUnit });

				annotationProcessor.process(Collections.<TypeElement>emptySet(), roundEnvironment);
			}

			if (providedLookupEnvironment != null && deregisterAtEnd) {
				int currentNestedCount = nestedCount.get();
				
				
				if (currentNestedCount == 1) {
//				&& (foundExistingLookupEnvironmentInfo == null
//					|| foundExistingLookupEnvironmentInfo.environment != providedLookupEnvironment)) {

					Set<CompilationUnitDeclaration> delayedCompilationUnitDeregistrationList =
							delayedCompilationUnitDeregistrations.get();
					if (delayedCompilationUnitDeregistrationList != null) {
						for (CompilationUnitDeclaration delayedCompilationUnitDeregistration
								: delayedCompilationUnitDeregistrationList) {
							EclipseProcessorUtils.deregisterTypeDeclarations(
									delayedCompilationUnitDeregistration, false);
						}
					}
					delayedCompilationUnitDeregistrations.set(null);

					EclipseProcessorUtils.deregisterTypeDeclarations(parsedUnit, false);
				} else {
					assert currentNestedCount > 1;

					Set<CompilationUnitDeclaration> delayedCompilationUnitDeregistrationList =
							delayedCompilationUnitDeregistrations.get();
					if (delayedCompilationUnitDeregistrationList == null) {
						delayedCompilationUnitDeregistrationList = new HashSet<CompilationUnitDeclaration>();
						delayedCompilationUnitDeregistrations.set(delayedCompilationUnitDeregistrationList);
					}

					delayedCompilationUnitDeregistrationList.add(parsedUnit);
				}
			}

			parsedUnit.bits |= ASTNode.Bit21;
		} finally {
			nestedCount.set(nestedCount.get() - 1);
			
//			if (Thread.currentThread().getName().equals("org.eclipse.jdt.internal.ui.text.JavaReconciler")
//					|| Thread.currentThread().getName().equals("main")) {
//				if (foundExistingLookupEnvironmentInfo != null) {
//					if (foundExistingLookupEnvironmentInfo.nestingCount != 0) {
//						foundExistingLookupEnvironmentInfo.nestingCount--;
//					} else {
//						existingLookupEnvironmentInfo.set(null);
//					}
//				}
//			}
		}
	}

	
	private static Processor createAnnotationProcessor(IServiceFactory processorFactory) {
		try {
			return (AbstractProcessor) processorFactory.newInstance();
		} catch (CoreException e) {
			throw new JxtendEclipseHookException(e);
		}
	}

	private static CompilerOptions createCompilerOptions(JavaProject javaProject) {
		EclipsePreferences eclipsePreferences = getEclipsePreferences(javaProject);
		String sourceLevel = getString(eclipsePreferences, "org.eclipse.jdt.core.compiler.source");

		Map<String, Object> compilerSettings = new HashMap<String, Object>();
		compilerSettings.put(CompilerOptions.OPTION_Source, sourceLevel);
		return new CompilerOptions(compilerSettings);
	}

	public static LookupEnvironment createLookupEnvironment(JavaProject javaProject, WorkingCopyOwner owner) {
		CompilerOptions compilerOptions = createCompilerOptions(javaProject);
		return createLookupEnvironment(javaProject, owner, compilerOptions);
	}

	private static LookupEnvironment createLookupEnvironment(
			JavaProject javaProject, WorkingCopyOwner owner, CompilerOptions compilerOptions) {
		try {
//			INameEnvironment nameEnvironment = new NameEnvironment(javaProject);
			SearchableEnvironment nameEnvironment = new SearchableEnvironment(javaProject, owner);
			IErrorHandlingPolicy errorHandlingPolicy = new ErrorHandlingPolicy();
			IProblemFactory problemFactory = new DefaultProblemFactory();
			Compiler compiler = createCompiler(nameEnvironment, errorHandlingPolicy,
					compilerOptions, problemFactory, nameEnvironment /*new SearchableEnvironment(javaProject, owner)*/, owner, javaProject);
			return compiler.lookupEnvironment;
//			return new LookupEnvironment(typeRequestor, compilerOptions,
//					new ProblemReporter(errorHandlingPolicy, compilerOptions, problemFactory), nameEnvironment);
		} catch (JavaModelException e) {
			throw new JxtendEclipseHookException(e);
		}
	}
	
	private static Compiler createCompiler(INameEnvironment nameEnvironment,
			IErrorHandlingPolicy errorHandlingPolicy, CompilerOptions compilerOptions, IProblemFactory problemFactory,
			SearchableEnvironment searchableEnvironment, WorkingCopyOwner owner, IJavaProject javaProject) {
//		return new Compiler(nameEnvironment, errorHandlingPolicy, compilerOptions,
//				new ICompilerRequestor() {
//					@Override public void acceptResult(CompilationResult result) {
//						// do nothing
//					}
//				},
//				problemFactory, null, null);
		Class<ITypeRequestor> clazz;
		try {
			clazz = (Class) Class.forName("org.eclipse.jdt.core.dom.CompilationUnitResolver");
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}

		Constructor<ITypeRequestor> constructor = ReflectUtils.getInaccessibleConstructor(clazz,
				INameEnvironment.class, IErrorHandlingPolicy.class, CompilerOptions.class, ICompilerRequestor.class,
				IProblemFactory.class, IProgressMonitor.class, boolean.class);
		try {
			return (Compiler) constructor.newInstance(nameEnvironment, errorHandlingPolicy, compilerOptions, 
					new ICompilerRequestor() {
						@Override public void acceptResult(CompilationResult result) {
							// do nothing
						}
					}, problemFactory, new NullProgressMonitor(), (javaProject != null));
		} catch (IllegalArgumentException e) {
			throw new RuntimeException(e);
		} catch (InstantiationException e) {
			throw new RuntimeException(e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(e);
		}
		
		
		
//						INameEnvironment environment,
//						IErrorHandlingPolicy policy,
//						CompilerOptions compilerOptions,
//						ICompilerRequestor requestor,
//						IProblemFactory problemFactory,
//						IProgressMonitor monitor,
//						boolean fromJavaProject) {
	}

	private static class ErrorHandlingPolicy implements IErrorHandlingPolicy {
		@Override
		public boolean proceedOnErrors() {
			return true;
		}

		@Override
		public boolean stopOnFirstError() {
			return false;
		}
	}	

	private static class HookProcessingEnvironment extends BaseProcessingEnvImpl {

		private final LookupEnvironment lookupEnvironment;

		private HookProcessingEnvironment(LookupEnvironment lookupEnvironment) {
			this.lookupEnvironment = lookupEnvironment;
		}

		@Override
		public Locale getLocale() {
			return Locale.getDefault();
		}

		@Override
		public LookupEnvironment getLookupEnvironment() {
			return lookupEnvironment;
		}
	}	

	//TODO: Remove in the future; However for this we need to make changes to the 'JxtendProcessor'
	private static class HookRoundEnvironment extends RoundEnvImpl {
		
		public HookRoundEnvironment(CompilationUnitDeclaration[] units, ReferenceBinding[] binaryTypeBindings,
				boolean isLastRound, BaseProcessingEnvImpl env) {
			super(units, binaryTypeBindings, isLastRound, env);
		}
	}

	private static class ExistingLookupEnvironmentInfo {
		LookupEnvironment environment;
		int nestingCount = 0;

		ExistingLookupEnvironmentInfo(LookupEnvironment environment) {
			super();
			this.environment = environment;
		}
	}
}
